package icasue.runninginvoke.register;

import icasue.runninginvoke.annotations.RunningInvokable;
import icasue.runninginvoke.exposebucket.RunningInvokeExposePointBucket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.*;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/7/29 下午3:09
 * @UpdateDate:
 * @Description:
 */
@Slf4j
public class RunningInvokeExposePointScanner extends ClassPathBeanDefinitionScanner {

    //ignore base JavaType's param mapping
    public static final List<String> ignoreBaseTypeDefaultValue = Arrays.asList(
            "int","float","double","boolean","char","byte","long","short"
    );
    //ignore reference JavaTypes.
    public static final List<Class> ignoreReferenceTypeDefaultValue = Arrays.asList(
            Map.class,Integer.class,Double.class,String.class,
            Float.class,Long.class,Short.class,
            Byte.class,Character.class, BigDecimal.class,
            Boolean.class,Enum.class, Annotation.class
    );


    private Class<RunningInvokable> invokeExposePoint = RunningInvokable.class;
    private List<String> excludeRegex = null;

    public RunningInvokeExposePointScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
        super(registry, useDefaultFilters);
    }

    public void registerFilters(){

        boolean acceptAll = true;

        if(acceptAll) {
            addIncludeFilter(new TypeFilter() {
                @Override
                public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                    return true;
                }
            });
        }
        if(excludeRegex != null && excludeRegex.size() > 0){
            for (String regex : excludeRegex) {
                addExcludeFilter(new TypeFilter() {
                    @Override
                    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
                        ClassMetadata classMetadata = metadataReader.getClassMetadata();
                        return classMetadata.getClassName().matches(regex);
                    }
                });
            }
            acceptAll = false;
        }
    }

    public void doScanGetExposePoint(List<String> basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinition> beanDefinitions = new LinkedHashSet<>();
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
            beanDefinitions.addAll(candidates);
        }
        for (BeanDefinition definition : beanDefinitions) {
            Map<Method, RunningInvokable> exposePointMap = new HashMap<>();
            String beanClassName = definition.getBeanClassName();
            try {
                Class<?> beanClass = Class.forName(beanClassName);
                Arrays.stream(beanClass.getDeclaredMethods()).forEach((method) -> {
                    Optional<RunningInvokable> runningInvokableOptional = Optional.ofNullable(method.getAnnotation(invokeExposePoint));
                    if(runningInvokableOptional.isPresent()){
                        exposePointMap.put(method,runningInvokableOptional.get());
                    }
                });
            }catch (Throwable e){
                log.info("Reflect get ClassMetadata fail find expose point will skip, className : {} ",beanClassName);
            }

            if(exposePointMap.size() > 0){
                for (Map.Entry<Method, RunningInvokable> entry : exposePointMap.entrySet()) {
                    //Getting expose point infos.
                    Method exposeMethod = entry.getKey();
                    RunningInvokable exposeMethodInfo = entry.getValue();
                    RunningInvokeExposePointBucket.InvokerBucket invokerBucketUnit =
                            new RunningInvokeExposePointBucket.InvokerBucket();
                    //Getting expose point's construct params formation.
                    String simpleMethodName = exposeMethod.getName().substring(exposeMethod.getName().lastIndexOf(".") + 1);
                    String exposePointDescription = !"".equals(exposeMethodInfo.description())
                            ? exposeMethodInfo.description()
                            : String.join("",exposeMethod.getDeclaringClass().getSimpleName()," : ",simpleMethodName);
                    List<RunningInvokeExposePointBucket.InvokerBucket.InvokerParams> invokerBucketUnitParams = invokerBucketUnit.getParams();
                    Class<?>[] parameterTypes = exposeMethod.getParameterTypes();
                    if(parameterTypes.length > 0){
                        String[] docCopyArray = Arrays.copyOf(exposeMethodInfo.docs(), parameterTypes.length);
                        for (int i = 0; i < parameterTypes.length; i++) {
                            String parameterClassName = parameterTypes[i].getName();
                            Object parameterValue = getParamDefaultValue(parameterClassName);
                            invokerBucketUnitParams.add(new RunningInvokeExposePointBucket.InvokerBucket.InvokerParams(
                                    docCopyArray[i] == null ? "" : docCopyArray[i],
                                    parameterClassName,
                                    parameterValue
                            ));
                        }
                    }
                    //CreateAdd expose point.
                    invokerBucketUnit.setClassName(exposeMethod.getDeclaringClass().getName());
                    invokerBucketUnit.setFunctionDescription(exposePointDescription);
                    invokerBucketUnit.setMethodName(simpleMethodName);
                    invokerBucketUnit.setParams(invokerBucketUnitParams);
                    RunningInvokeExposePointBucket.exposePointBucket.add(invokerBucketUnit);
                }
            }
        }
    }

    private Object getParamDefaultValue(String parameterClassName){
        //ignore object
        if(parameterClassName.equals("java.lang.Object")){
            return null;
        }
        //ignore base JavaType.
        if(ignoreBaseTypeDefaultValue.contains(parameterClassName)){
            return null;
        }
        Class<?> ofClass = null;
        try {
            ofClass = Class.forName(parameterClassName);
        } catch (Throwable e) { /**类名从扫描的BeanDefinition上获取的，无异常**/}
        //ignore reference JavaType.
        for (Class ignoreClass : ignoreReferenceTypeDefaultValue) {
            if(ignoreClass.isAssignableFrom(ofClass)){
                return null;
            }
        }
        //ignore Array,Enum,Package-Info,Annotation.
        if(ofClass.isArray() || ofClass.isEnum() || ofClass.isAnnotation() || ofClass.isAnonymousClass()){
            return null;
        }
        //current param exist as an array.
        if(List.class.isAssignableFrom(ofClass)){
            return Arrays.asList(new HashMap<String,Object>());
        }
        //try getting instance,
        // if getting fail means this special JavaType's primary construct's privilege is limited
        // or not has exist construct. at this time, through java reflect construct this complicate JavaType will
        // failed in virtual invoke , so this expose point should be ignore.
        // but actually, we do not keep reflect fail's ignore logic.
        try {
            return ofClass.newInstance();
        } catch (Throwable e) {
            //reflect param model fail, ignore this param's model, return null.
            return null;
        }
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        return true;
    }

    public List<String> getExcludeRegex() {
        return excludeRegex;
    }

    public void setExcludeRegex(List<String> excludeRegex) {
        this.excludeRegex = excludeRegex;
    }
}
